module(..., package.seeall)

-----------------------------------------------------------------------------
-- This is the reference implementation for an authentication module,
-- implementing all of the core functionality will expect from
-- a drop-in-replacement
-----------------------------------------------------------------------------

local errors = require("zwsapi.auth.errors")

local Simple = {}
local Simple_mt = {__metatable = {}, __index = Simple}

-- Utility functions
local function load_users(dal, name)
	local node = dal:GetNode(name)
	return node.content.USERS, node.__raw_v.content
end

local function get_salted_hash(time, salt, password)
	return md5.sumhexa(time .. salt .. password)
end

local function user_token(user, salt, hash)
	return md5.sumhexa(user .. salt .. "zwsapi")
end

-----------------------------------------------------------------------------
-- Creates a new instance of the authentication module for use in dal
--
-- @param dal		  a dal instance to use for storage .
-- @param params			a table of configuration paramaters (the actual set
--							  of parameters is implementation-specific.
-----------------------------------------------------------------------------
function new(dal, params)
	-- Set up default parameters
	params = params or {}
	params.node = params.node or "@ws/passwords"
	params.password_salt = params.password_salt or dal.config.PASSWORD_SALT or 'PW'
	params.token_salt = params.token_salt or dal.config.TOKEN_SALT or 'TK'
	params.recent = params.recent or (14 * 24 * 60 * 60)

	local obj = setmetatable({}, Simple_mt)
	obj.dal = dal
	obj.node = params.node
	obj.password_salt = params.password_salt
	obj.token_salt = params.token_salt
	obj.noauto = params.NO_AUTO_REGISTRATION
	obj.recent = params.recent
	obj.users = load_users(obj.dal, obj.node)
	return obj
end

------------------------------------------------------------------
-- Returns whether or not a given username exists in the 
-- authentication system, without any further information
--
-- @param username the username to query
-- @return exists whether or not the username exists in the system

function Simple:user_exists(dal,username)
	if type(username) ~= "string" then return false end
	username=username:lower()
	return type(self.users[username]) == "table"
end

------------------------------------------------------------------
-- Returns a token for the specified timestamp.  This is provided
-- for use outside the authentication system
--
-- @param timestamp - the timestamp to use when generating token
-- @return token a hashed token representing the given timestamp

function Simple:Timestamp_token(timestamp)
	return md5.sumhexa(timestamp .. self.token_salt)
end

------------------------------------------------------------------
-- Attempt to authenticate a given user with a given password
--
-- @param user the username to authenticate
-- @param password the raw password to authenticate with
-- @return user the name of the authenticated user
-- @return token a hashed token for the user

function Simple:Authenticate(dal,username, password)
	username = username:lower()
	local entry = self.users[username]
	if entry then
		local hash = get_salted_hash(entry.creation_time, self.password_salt, password)
		if hash == entry.hash then 
			return entry.display, user_token(username, self.token_salt, entry.hash)
		else
			return nil, errors.wrong_password(username)
		end
	else
		return nil, errors.no_such_user(username)
	end
end

------------------------------------------------------------------
-- Validate an existing authentication token.  This is used for 
-- allowing authentication via cookies
--
-- @param user the username the token belong to
-- @param token the actual token hash
-- @return user the name of the authenticated user

function Simple:Validate_token(dal,username, token)
	username = username:lower()
	local entry = self.users[username]

	if self:user_exists(dal,username) then
		if user_token(username, self.token_salt, entry.hash) == token then
			return entry.display
		else
			return false, errors.wrong_password(username)
		end
	else
		return false, errors.no_such_user(username)
	end
end

------------------------------------------------------------------
-- Returns whether or not a given user is a new user, defined
-- by the "recent" configuration parameter.
-- @param user the username to query
-- @return isRecent a boolean value indicating if the user's 
-- account was created in the specified time frame

function Simple:user_is_recent(username)
	username = username:lower()
	local entry = self.users[username]

	if entry then
		local now = os.time()
		local min = now - self.recent

		return (tonumber(entry.creation_time) > min)
	else
		return nil, errors.no_such_user(username)
	end
end

------------------------------------------------------------------
-- Adds a user/password pair to the password file
--
-- @param user the username to add
-- @param password the raw password
-- @return success a boolean value indicating if the add was 
-- successful.
-- @return err an error message if the add was not successful
function Simple:add_user(dal,username, password, metadata)

	local now = os.time()
	local users, raw_users = load_users(self.dal, self.node)
  
	if self:user_exists(dal,username) then
		return nil, errors.user_already_exist(username)
	end

	metadata = metadata or {}
	metadata.creation_time = now
	metadata.display = username
	metadata.hash = get_salted_hash(now, self.password_salt, password)
	username = username:lower()
	if username == "admin" then
		metadata.is_admin = "true"
	end

	users[username] = metadata

	local user_as_string = string.format("USERS[%q]={", username)
	for k,v in pairs(metadata) do
		user_as_string = user_as_string..string.format(" %s=%q,", k, v)
	end
	user_as_string = user_as_string.."}"

	local password_node = self.dal:GetNode(self.node)

	local params = {
		content = (raw_users or "USERS={}\n").."\n"..user_as_string,
	}
	password_node:__update_fields(params)
	password_node:__save()
	self.users = load_users(self.dal, self.node)
	return true
end
function Simple:chg_pwd(user,dal,username, password, newpwd)
	if not username or username=="" then return nil end
	username=username:lower()
	entry=self.users[username]
	if entry then
		local auok
		if user~=username then
			if self:Get_metadata(dal,user, "is_admin") then
				auok=self:Authenticate(dal,user,password)
				if not auok then
					return nil,errors.wrong_password(user)
				end
			else
				return nil,"Authentication Error: No permission"
			end
		else
			auok=self:Authenticate(dal,user,password)
		end
		if auok then 
			entry.hash=get_salted_hash(entry.creation_time, self.password_salt, newpwd)
			local password_node = self.dal:GetNode(self.node)
			local params = {
				content = '\nUSERS={}\nUSERS='..zdc.doc.serialize(password_node.content.USERS),
			}
			password_node:__update_fields(params)
			password_node:__save()
			self.users = load_users(self.dal, self.node)
			return true
		else
			return nil,errors.wrong_password(username)
		end
	else 
		return nil,errors.no_such_user(username)
	end
end
-----------------------------------------------------------------------------
-- Retrieves a piece of metadata for a specific user
--
-- @param username		 the username to query
-- @param key				the metadata key to query
-- @return data			 the value of the metadata or nil
function Simple:Get_metadata(dal,username, key)
	if not username or username=="" then return nil end
	username=username:lower()
	if self.users[username] then 
		return self.users[username][key]
	else
		return nil,errors.no_such_user(username)
	end
end

